Atklājiet patiesu daudzpavedienu darbību JavaScript. Šis visaptverošais ceļvedis aptver SharedArrayBuffer, Atomics, Web Workers un drošības prasības augstas veiktspējas tīmekļa lietojumprogrammām.
JavaScript SharedArrayBuffer: Dziļāka iedziļināšanās vienlaicīgajā programmēšanā tīmeklī
Gadu desmitiem JavaScript viena pavediena daba ir bijusi gan tās vienkāršības avots, gan būtisks veiktspējas šķērslis. Notikumu cikla modelis lieliski darbojas vairumam ar lietotāja saskarni saistītu uzdevumu, taču tas saskaras ar grūtībām, kad jāveic skaitļošanas ziņā intensīvas operācijas. Ilgstoši aprēķini var "iesaldēt" pārlūkprogrammu, radot nepatīkamu lietotāja pieredzi. Lai gan Web Workers piedāvāja daļēju risinājumu, ļaujot skriptiem darboties fonā, tiem bija savs būtisks ierobežojums: neefektīva datu komunikācija.
Ienāk SharedArrayBuffer
(SAB) – jaudīga funkcija, kas fundamentāli maina spēles noteikumus, ieviešot patiesu, zema līmeņa atmiņas koplietošanu starp pavedieniem tīmeklī. Savienojumā ar Atomics
objektu, SAB paver jaunu ēru augstas veiktspējas, vienlaicīgām lietojumprogrammām tieši pārlūkprogrammā. Tomēr ar lielu varu nāk liela atbildība un sarežģītība.
Šis ceļvedis jūs aizvedīs dziļākā ceļojumā vienlaicīgās programmēšanas pasaulē JavaScript valodā. Mēs izpētīsim, kāpēc mums tas ir nepieciešams, kā darbojas SharedArrayBuffer
un Atomics
, kritiskos drošības apsvērumus, kas jums jārisina, un praktiskus piemērus, lai jūs varētu sākt darbu.
Vecā pasaule: JavaScript viena pavediena modelis un tā ierobežojumi
Pirms mēs varam novērtēt risinājumu, mums pilnībā jāizprot problēma. JavaScript izpilde pārlūkprogrammā tradicionāli notiek vienā pavedienā, ko bieži sauc par "galveno pavedienu" vai "UI pavedienu".
Notikumu cikls
Galvenais pavediens ir atbildīgs par visu: jūsu JavaScript koda izpildi, lapas renderēšanu, atbildi uz lietotāja mijiedarbībām (piemēram, klikšķiem un ritināšanu) un CSS animāciju izpildi. Tas pārvalda šos uzdevumus, izmantojot notikumu ciklu, kas nepārtraukti apstrādā ziņojumu (uzdevumu) rindu. Ja uzdevuma pabeigšana aizņem ilgu laiku, tas bloķē visu rindu. Nekas cits nevar notikt – lietotāja saskarne sasalst, animācijas raustās, un lapa kļūst nereaģējoša.
Web Workers: solis pareizajā virzienā
Web Workers tika ieviesti, lai mazinātu šo problēmu. Web Worker būtībā ir skripts, kas darbojas atsevišķā fona pavedienā. Jūs varat pārcelt smagus aprēķinus uz "worker", atstājot galveno pavedienu brīvu, lai apstrādātu lietotāja saskarni.
Komunikācija starp galveno pavedienu un "worker" notiek, izmantojot postMessage()
API. Kad jūs nosūtāt datus, tos apstrādā strukturētās klonēšanas algoritms. Tas nozīmē, ka dati tiek serializēti, kopēti un pēc tam deserializēti "worker" kontekstā. Lai gan šis process ir efektīvs, tam ir būtiski trūkumi lielu datu kopu gadījumā:
- Veiktspējas slogs: Megabaitu vai pat gigabaitu datu kopēšana starp pavedieniem ir lēna un CPU ietilpīga.
- Atmiņas patēriņš: Tas izveido datu dublikātu atmiņā, kas var būt liela problēma ierīcēm ar ierobežotu atmiņu.
Iedomājieties video redaktoru pārlūkprogrammā. Visa video kadra (kas var būt vairāki megabaiti) sūtīšana uz "worker" un atpakaļ apstrādei 60 reizes sekundē būtu pārmērīgi dārga. Tieši šo problēmu SharedArrayBuffer
tika izstrādāts, lai atrisinātu.
Spēles noteikumu mainītājs: Iepazīstinām ar SharedArrayBuffer
SharedArrayBuffer
ir fiksēta garuma neapstrādātu bināro datu buferis, līdzīgs ArrayBuffer
. Kritiskā atšķirība ir tā, ka SharedArrayBuffer
var tikt koplietots starp vairākiem pavedieniem (piemēram, galveno pavedienu un vienu vai vairākiem Web Workers). Kad jūs "nosūtāt" SharedArrayBuffer
, izmantojot postMessage()
, jūs nesūtāt kopiju; jūs sūtāt atsauci uz to pašu atmiņas bloku.
Tas nozīmē, ka jebkuras izmaiņas, ko bufera datos veic viens pavediens, ir nekavējoties redzamas visiem pārējiem pavedieniem, kuriem ir atsauce uz to. Tas novērš dārgo kopēšanas un serializācijas soli, nodrošinot gandrīz tūlītēju datu koplietošanu.
Iedomājieties to šādi:
- Web Workers ar
postMessage()
: Tas ir kā divi kolēģi, kas strādā pie dokumenta, sūtot kopijas viens otram pa e-pastu. Katra izmaiņa prasa nosūtīt pilnīgi jaunu kopiju. - Web Workers ar
SharedArrayBuffer
: Tas ir kā divi kolēģi, kas strādā pie tā paša dokumenta koplietotā tiešsaistes redaktorā (piemēram, Google Docs). Izmaiņas abiem ir redzamas reāllaikā.
Koplietotās atmiņas bīstamība: sacensību apstākļi
Tūlītēja atmiņas koplietošana ir jaudīga, taču tā arī ievieš klasisku problēmu no vienlaicīgās programmēšanas pasaules: sacensību apstākļus (race conditions).
Sacensību apstākļi rodas, kad vairāki pavedieni mēģina vienlaicīgi piekļūt un modificēt tos pašus koplietotos datus, un galīgais rezultāts ir atkarīgs no neparedzamās secības, kādā tie tiek izpildīti. Apsveriet vienkāršu skaitītāju, kas glabājas SharedArrayBuffer
. Gan galvenais pavediens, gan "worker" vēlas to palielināt.
- Pavediens A nolasa pašreizējo vērtību, kas ir 5.
- Pirms pavediens A var ierakstīt jauno vērtību, operētājsistēma to aptur un pārslēdzas uz pavedienu B.
- Pavediens B nolasa pašreizējo vērtību, kas joprojām ir 5.
- Pavediens B aprēķina jauno vērtību (6) un ieraksta to atpakaļ atmiņā.
- Sistēma pārslēdzas atpakaļ uz pavedienu A. Tas nezina, ka pavediens B kaut ko ir izdarījis. Tas turpina no vietas, kur apstājās, aprēķinot savu jauno vērtību (5 + 1 = 6) un ierakstot 6 atpakaļ atmiņā.
Lai gan skaitītājs tika palielināts divas reizes, gala vērtība ir 6, nevis 7. Operācijas nebija atomāras – tās bija pārtraucamas, kas noveda pie datu zuduma. Tieši tāpēc jūs nevarat izmantot SharedArrayBuffer
bez tā svarīgākā partnera: Atomics
objekta.
Koplietotās atmiņas sargs: Atomics
objekts
Atomics
objekts nodrošina statisku metožu kopu, lai veiktu atomāras operācijas ar SharedArrayBuffer
objektiem. Atomāra operācija garantēti tiek veikta pilnībā, netiekot pārtraukta ar citām operācijām. Tā vai nu notiek pilnībā, vai nenotiek vispār.
Atomics
izmantošana novērš sacensību apstākļus, nodrošinot, ka lasīšanas-modificēšanas-rakstīšanas operācijas ar koplietoto atmiņu tiek veiktas droši.
Galvenās Atomics
metodes
Apskatīsim dažas no svarīgākajām metodēm, ko nodrošina Atomics
.
Atomics.load(typedArray, index)
: Atomāri nolasa vērtību norādītajā indeksā un atgriež to. Tas nodrošina, ka jūs lasāt pilnīgu, nebojātu vērtību.Atomics.store(typedArray, index, value)
: Atomāri saglabā vērtību norādītajā indeksā un atgriež šo vērtību. Tas nodrošina, ka rakstīšanas operācija netiek pārtraukta.Atomics.add(typedArray, index, value)
: Atomāri pieskaita vērtību pie vērtības norādītajā indeksā. Tā atgriež sākotnējo vērtību šajā pozīcijā. Tas ir atomārs ekvivalentsx += value
.Atomics.sub(typedArray, index, value)
: Atomāri atņem vērtību no vērtības norādītajā indeksā.Atomics.compareExchange(typedArray, index, expectedValue, replacementValue)
: Šī ir jaudīga nosacījuma rakstīšana. Tā pārbauda, vai vērtībaindex
pozīcijā ir vienāda arexpectedValue
. Ja tā ir, tā to aizstāj arreplacementValue
un atgriež sākotnējoexpectedValue
. Ja nē, tā nedara neko un atgriež pašreizējo vērtību. Tas ir fundamentāls būvelements, lai ieviestu sarežģītākus sinhronizācijas primitīvus, piemēram, slēdzenes (locks).
Sinhronizācija: Vairāk nekā vienkāršas operācijas
Dažreiz ir nepieciešams vairāk nekā tikai droša lasīšana un rakstīšana. Ir nepieciešams, lai pavedieni koordinētu savu darbību un gaidītu viens uz otru. Izplatīts slikts paņēmiens ir "aktīvā gaidīšana" (busy-waiting), kur pavediens atrodas ciklā, nepārtraukti pārbaudot atmiņas vietu, vai nav notikušas izmaiņas. Tas tērē CPU ciklus un izlādē akumulatoru.
Atomics
nodrošina daudz efektīvāku risinājumu ar wait()
un notify()
.
Atomics.wait(typedArray, index, value, timeout)
: Šī metode liek pavedienam "aizmigt". Tā pārbauda, vai vērtībaindex
pozīcijā joprojām irvalue
. Ja tā, pavediens guļ, līdz to pamodina arAtomics.notify()
vai līdz beidzas izvēlestimeout
(milisekundēs). Ja vērtībaindex
pozīcijā jau ir mainījusies, tā nekavējoties atgriežas. Tas ir neticami efektīvi, jo gulošs pavediens patērē gandrīz nekādus CPU resursus.Atomics.notify(typedArray, index, count)
: To izmanto, lai pamodinātu pavedienus, kas "guļ", gaidot uz noteiktu atmiņas vietu, izmantojotAtomics.wait()
. Tā pamodinās ne vairāk kācount
gaidošos pavedienus (vai visus, jacount
nav norādīts vai irInfinity
).
Saliekam visu kopā: Praktisks ceļvedis
Tagad, kad mēs saprotam teoriju, apskatīsim soļus, kā ieviest risinājumu, izmantojot SharedArrayBuffer
.
1. solis: Drošības priekšnoteikums – starpizcelsmes izolācija
Šis ir visbiežākais klupšanas akmens izstrādātājiem. Drošības apsvērumu dēļ SharedArrayBuffer
ir pieejams tikai lapās, kas atrodas starpizcelsmes izolētā stāvoklī. Tas ir drošības pasākums, lai mazinātu spekulatīvās izpildes ievainojamības, piemēram, Spectre, kas potenciāli varētu izmantot augstas izšķirtspējas taimerus (ko nodrošina koplietotā atmiņa), lai nopludinātu datus starp dažādām izcelsmēm.
Lai iespējotu starpizcelsmes izolāciju, jums jākonfigurē savs tīmekļa serveris, lai tas galvenajam dokumentam nosūtītu divas specifiskas HTTP galvenes:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
(COOP): Izolē jūsu dokumenta pārlūkošanas kontekstu no citiem dokumentiem, neļaujot tiem tieši mijiedarboties ar jūsu loga objektu.Cross-Origin-Embedder-Policy: require-corp
(COEP): Prasa, lai visi apakšresursi (piemēram, attēli, skripti un iframe), ko ielādē jūsu lapa, būtu vai nu no tās pašas izcelsmes, vai arī skaidri marķēti kā starpizcelsmes ielādējami arCross-Origin-Resource-Policy
galveni vai CORS.
To var būt sarežģīti iestatīt, īpaši, ja jūs paļaujaties uz trešo pušu skriptiem vai resursiem, kas nenodrošina nepieciešamās galvenes. Pēc servera konfigurēšanas jūs varat pārbaudīt, vai jūsu lapa ir izolēta, pārbaudot self.crossOriginIsolated
īpašību pārlūkprogrammas konsolē. Tai jābūt true
.
2. solis: Bufera izveide un koplietošana
Jūsu galvenajā skriptā jūs izveidojat SharedArrayBuffer
un "skatu" uz to, izmantojot TypedArray
, piemēram, Int32Array
.
main.js:
// Vispirms pārbaudiet starpizcelsmes izolāciju!
if (!self.crossOriginIsolated) {
console.error("Šī lapa nav starpizcelsmes izolēta. SharedArrayBuffer nebūs pieejams.");
} else {
// Izveidojiet koplietojamu buferi vienam 32 bitu veselam skaitlim.
const buffer = new SharedArrayBuffer(4);
// Izveidojiet skatu uz buferi. Visas atomārās operācijas notiek uz skata.
const int32Array = new Int32Array(buffer);
// Inicializējiet vērtību indeksā 0.
int32Array[0] = 0;
// Izveidojiet jaunu "worker".
const worker = new Worker('worker.js');
// Nosūtiet KOPLIETOJAMO buferi uz "worker". Tā ir atsauces nodošana, nevis kopija.
worker.postMessage({ buffer });
// Klausieties ziņojumus no "worker".
worker.onmessage = (event) => {
console.log(`Worker paziņoja par pabeigšanu. Gala vērtība: ${Atomics.load(int32Array, 0)}`);
};
}
3. solis: Atomāro operāciju veikšana "worker"
"Worker" saņem buferi un tagad var veikt atomāras operācijas ar to.
worker.js:
self.onmessage = (event) => {
const { buffer } = event.data;
const int32Array = new Int32Array(buffer);
console.log("Worker saņēma koplietojamo buferi.");
// Veiksim dažas atomāras operācijas.
for (let i = 0; i < 1000000; i++) {
// Droši palieliniet koplietoto vērtību.
Atomics.add(int32Array, 0, 1);
}
console.log("Worker pabeidza palielināšanu.");
// Signalizējiet galvenajam pavedienam, ka esam pabeiguši.
self.postMessage({ done: true });
};
4. solis: Sarežģītāks piemērs – paralēla summēšana ar sinhronizāciju
Pievērsīsimies reālistiskākai problēmai: ļoti liela skaitļu masīva summēšanai, izmantojot vairākus "workers". Mēs izmantosim Atomics.wait()
un Atomics.notify()
efektīvai sinhronizācijai.
Mūsu koplietotajam buferim būs trīs daļas:
- Indekss 0: Statusa karodziņš (0 = apstrādā, 1 = pabeigts).
- Indekss 1: Skaitītājs, cik "workers" ir pabeiguši darbu.
- Indekss 2: Gala summa.
main.js:
if (self.crossOriginIsolated) {
const NUM_WORKERS = 4;
const DATA_SIZE = 10_000_000;
// [statuss, pabeigušie_workers, rezultāts_zemais, rezultāts_augstais]
// Mēs izmantojam divus 32 bitu veselos skaitļus rezultātam, lai izvairītos no pārpildes lielām summām.
const sharedBuffer = new SharedArrayBuffer(4 * 4); // 4 veseli skaitļi
const sharedArray = new Int32Array(sharedBuffer);
// Ģenerējiet dažus nejaušus datus apstrādei
const data = new Uint8Array(DATA_SIZE);
for (let i = 0; i < DATA_SIZE; i++) {
data[i] = Math.floor(Math.random() * 10);
}
const chunkSize = Math.ceil(DATA_SIZE / NUM_WORKERS);
for (let i = 0; i < NUM_WORKERS; i++) {
const worker = new Worker('sum_worker.js');
const start = i * chunkSize;
const end = Math.min(start + chunkSize, DATA_SIZE);
// Izveidojiet nekoplietotu skatu "worker" datu daļai
const dataChunk = data.subarray(start, end);
worker.postMessage({
sharedBuffer,
dataChunk // Tas tiek kopēts
});
}
console.log('Galvenais pavediens tagad gaida, kad "workers" pabeigs...');
// Gaidiet, kamēr statusa karodziņš indeksā 0 kļūs par 1
// Tas ir daudz labāk nekā while cikls!
Atomics.wait(sharedArray, 0, 0); // Gaidīt, ja sharedArray[0] ir 0
console.log('Galvenais pavediens pamodināts!');
const finalSum = Atomics.load(sharedArray, 2);
console.log(`Gala paralēlā summa ir: ${finalSum}`);
} else {
console.error('Lapa nav starpizcelsmes izolēta.');
}
sum_worker.js:
self.onmessage = ({ data }) => {
const { sharedBuffer, dataChunk } = data;
const sharedArray = new Int32Array(sharedBuffer);
// Aprēķiniet summu šī "worker" datu daļai
let localSum = 0;
for (let i = 0; i < dataChunk.length; i++) {
localSum += dataChunk[i];
}
// Atomāri pieskaitiet vietējo summu kopējai summai
Atomics.add(sharedArray, 2, localSum);
// Atomāri palieliniet 'pabeigušo workers' skaitītāju
const finishedCount = Atomics.add(sharedArray, 1, 1) + 1;
// Ja šis ir pēdējais "worker", kas pabeidz...
const NUM_WORKERS = 4; // Reālā lietojumprogrammā būtu jānodod kā parametrs
if (finishedCount === NUM_WORKERS) {
console.log('Pēdējais worker pabeidza. Paziņo galvenajam pavedienam.');
// 1. Iestatiet statusa karodziņu uz 1 (pabeigts)
Atomics.store(sharedArray, 0, 1);
// 2. Paziņojiet galvenajam pavedienam, kas gaida uz indeksu 0
Atomics.notify(sharedArray, 0, 1);
}
};
Reālās pasaules lietošanas gadījumi un pielietojumi
Kur šī jaudīgā, bet sarežģītā tehnoloģija patiešām rada atšķirību? Tā izceļas lietojumprogrammās, kas prasa smagus, paralēli veicamus aprēķinus ar lielām datu kopām.
- WebAssembly (Wasm): Šis ir galvenais lietošanas gadījums. Valodām, piemēram, C++, Rust un Go, ir nobriedis atbalsts daudzpavedienu darbībai. Wasm ļauj izstrādātājiem kompilēt šīs esošās augstas veiktspējas, daudzpavedienu lietojumprogrammas (piemēram, spēļu dzinējus, CAD programmatūru un zinātniskos modeļus), lai tās darbotos pārlūkprogrammā, izmantojot
SharedArrayBuffer
kā pamatmehānismu pavedienu komunikācijai. - Datu apstrāde pārlūkprogrammā: Liela mēroga datu vizualizāciju, klienta puses mašīnmācīšanās modeļu secināšanu un zinātniskās simulācijas, kas apstrādā milzīgu datu apjomu, var ievērojami paātrināt.
- Multivides rediģēšana: Filtru piemērošanu augstas izšķirtspējas attēliem vai audio apstrādi skaņas failam var sadalīt daļās un apstrādāt paralēli ar vairākiem "workers", nodrošinot lietotājam reāllaika atgriezenisko saiti.
- Augstas veiktspējas spēles: Mūsdienu spēļu dzinēji lielā mērā paļaujas uz daudzpavedienu darbību fizikai, mākslīgajam intelektam un resursu ielādei.
SharedArrayBuffer
ļauj veidot konsoles kvalitātes spēles, kas pilnībā darbojas pārlūkprogrammā.
Izaicinājumi un noslēguma apsvērumi
Lai gan SharedArrayBuffer
ir transformējošs, tas nav brīnumlīdzeklis. Tas ir zema līmeņa rīks, kas prasa rūpīgu apiešanos.
- Sarežģītība: Vienlaicīgā programmēšana ir bēdīgi slavena ar savu sarežģītību. Sacensību apstākļu un strupsceļu (deadlocks) atkļūdošana var būt neticami grūta. Jums ir jādomā citādi par to, kā tiek pārvaldīts jūsu lietojumprogrammas stāvoklis.
- Strupsceļi: Strupsceļš rodas, kad divi vai vairāki pavedieni tiek bloķēti uz visiem laikiem, katrs gaidot, kad otrs atbrīvos resursu. Tas var notikt, ja jūs nepareizi ieviešat sarežģītus bloķēšanas mehānismus.
- Drošības slogs: Starpizcelsmes izolācijas prasība ir būtisks šķērslis. Tā var salauzt integrācijas ar trešo pušu pakalpojumiem, reklāmām un maksājumu vārtejām, ja tās neatbalsta nepieciešamās CORS/CORP galvenes.
- Nav paredzēts katrai problēmai: Vienkāršiem fona uzdevumiem vai I/O operācijām tradicionālais Web Worker modelis ar
postMessage()
bieži ir vienkāršāks un pietiekams. IzmantojietSharedArrayBuffer
tikai tad, ja jums ir skaidrs, ar CPU saistīts šķērslis, kas ietver lielu datu apjomu.
Noslēgums
SharedArrayBuffer
kopā ar Atomics
un Web Workers pārstāv paradigmas maiņu tīmekļa izstrādē. Tas sagrauj viena pavediena modeļa robežas, aicinot pārlūkprogrammā jaunu klasi jaudīgu, veiktspējīgu un sarežģītu lietojumprogrammu. Tas nostāda tīmekļa platformu līdzvērtīgākā pozīcijā ar natīvo lietojumprogrammu izstrādi skaitļošanas ziņā intensīviem uzdevumiem.
Ceļojums vienlaicīgajā JavaScript ir izaicinošs, prasot stingru pieeju stāvokļa pārvaldībai, sinhronizācijai un drošībai. Bet izstrādātājiem, kas vēlas paplašināt tīmekļa iespēju robežas – no reāllaika audio sintēzes līdz sarežģītai 3D renderēšanai un zinātniskai skaitļošanai – SharedArrayBuffer
apguve vairs nav tikai opcija; tā ir būtiska prasme nākamās paaudzes tīmekļa lietojumprogrammu veidošanai.